#!/usr/bin/env python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

import re
import os
import sys
import json
import traceback
from ibus import keysyms

JSON_SUFFIX = '.json'
VERSION_INFO = 'Version-Info.json'
API_PATH_PREFIX = 'aliyun-api-metadata-'

LICENSE = '''/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
'''

class TypeInfo:
    def __init__(self, meta, name, prim, dval, include):
        self.meta = meta
        self.name = name
        self.primitive = prim
        self.defaultValue = dval # default value
        self.include = include # for include line

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        return '{meta: %s, type: %s, default: %s, include: %s}' \
            % (self.meta, self.name, self.defaultValue, self.include)

basicTypes = [
    ('Boolean', 'bool',        True, 'false', None),
    ('Float',   'float',       True, '0.0f',  None),
    ('Integer', 'int',         True, '0',     None),
    ('Long',    'long',        True, '0L',    None),
    ('String',  'std::string', False, None,  '<string>'),
]

typeTable = {}
for t in basicTypes:
    typeTable[t[0]] = TypeInfo(t[0], t[1], t[2], t[3], t[4])

def registerType(index, type):
    if typeTable.get(index) != None:
        return False
    typeTable[index] = type
    return True

def lookupType(meta):
    for t in typeTable:
        if t == meta:
            return typeTable[t]
    return None


class PackageInfo:
    def __init__(self, path, product, version, apiStyle):
        self.path = path # json path
        self.product = product
        self.version = version
        self.apiStyle = apiStyle
        self.apiList = []

    def addApi(self, api):
        self.apiList.append(api)

    def addApiList(self, apis):
        self.apiList.extend(apis)

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        return '{path: %s, product: %s, version: %s, apiStyle: %s, apiList: %s}' \
            % (self.path, self.product, self.version, self.apiStyle, str(self.apiList))


# for API info
class API:
    def __init__(self, apiname, product, version, methods, protocols, pattern):
        # meta info
        self.apiname = apiname
        self.product = product
        self.version = version
        self.methods = methods     # request methods, GET, POST, etc.
        self.protocols = protocols # request protocol, HTTP, HTTP, etc.
        self.pattern = pattern   # request pattern(optional)


class Field:
    def __init__(self, m, n, t):
        self.meta = m # meta name
        self.name = n # pretty name
        self.type = t


class ApiClass:
    def __init__(self, root, api, apiname, isRequest, package):
        self.root = root
        self.api = api
        if isRequest:
            self.name = apiname + 'Request'
        else:
            self.name = apiname + 'Response'
        self.apiname = apiname
        self.package = package
        self.path = None # path will write to
        self.isRequest = isRequest
        self.includes = set()
        self.subtypes = []
        self.fields = []
        self.lines = []

    def addSubType(self, subtype):
        self.subtypes.append(subtype)

    def addInclude(self, include):
        self.includes.add(include)

    def sortIncludes(self):
        cheaders = []
        cxxheaders = []
        projectHeaders = []
        
        cpat = re.compile('<.*\.h>') # c header pattern
        syspat = re.compile('<.*>') # c & c++ header pattern
        for i in self.includes:
            if re.match(syspat, i) != None:
                if re.match(cpat, i) != None:
                    cheaders.append(i)
                else:
                    cxxheaders.append(i)
            else:
                projectHeaders.append(i)

        cheaders.sort()
        cxxheaders.sort()
        projectHeaders.sort()
        cheaders.extend(cxxheaders)
        cheaders.extend(projectHeaders)
        return cheaders

    def addField(self, metatype, name):
        t = lookupType(metatype)
        assert t != None
        # lowerize head char
        meta = name
        name = name[0].lower() + name[1:] + '_'
        self.fields.append(Field(meta, name, t))
        if not t.primitive:
            self.addInclude(t.include)

    def prepareClassBody(self):
        lines = []
        # for class define
        baseType = ''
        if self.isRequest:
            apiStyle = self.package.apiStyle.capitalize()
            responseType = self.apiname + 'Response'
            baseType = '%sAcsRequest<%s>' % (apiStyle, responseType)
            self.addInclude('"%sAcsRequest.h"' % apiStyle)
            product = self.package.product.lower().replace('-', '_')
            version = 'v' + self.package.version.replace('-', '')
            self.addInclude('"%s/%s/%s.h"' % (product, version, responseType))
        else:
            baseType = 'AcsResponse'
            self.addInclude('"AcsResponse.h"')
        baseList = ' : public %s' % baseType
        lines.append('class %s%s {' % (self.name, baseList))
        lines.append(' public:')
        lines.append('')
        
        # ctor, for primitive type initialize and supper ctor
        if self.isRequest:
            lines.append('  typedef %s supper;' % baseType)
            lines.append('')
        initPart = ''
        if self.isRequest:
            initPart = '\n      : supper("%s", "%s", "%s")' % (self.package.product, self.package.version, self.apiname)
        lines.append('  %s()%s {' % (self.name, initPart)) # start ctor
        for f in self.fields:
            if f.type.primitive:
                lines.append('    %s = %s;' % (f.name, f.type.defaultValue))
        
        # for request protocol and method
        if self.isRequest:
            if len(self.api.methods) == 1:
                lines.append('    this->setMethod("%s");' % self.api.methods[0])
            if len(self.api.protocols) == 1:
                lines.append('    this->setProtocol("%s");' % self.api.protocols[0])
            if self.api.pattern != None:
                lines.append('    this->setUriPattern("%s");' % self.api.pattern)
        
        lines.append('  }') # end ctor
        lines.append('')
            # getters and setters
        for f in self.fields:
            lines.append('  %s get%s() const {' % (f.type.name, f.meta))
            lines.append('    return %s;' % f.name)
            lines.append('  }')
            lines.append('')
            lines.append('  void set%s(%s arg) {' % (f.meta, f.type.name))
            lines.append('    %s = arg;' % f.name)
            param = 'arg'
            if f.type.primitive:
                param = 'aliyun::utils::ToString(arg)'
                self.addInclude('"utils/StringUtils.h"')
            lines.append('    putQueryParameter("%s", %s);' % (f.meta, param))
            lines.append('  }')
            lines.append('')
        
        if not self.isRequest:
            lines.append('  IMPLEMENT_CLASSNAME(%s)' % self.name)
            lines.append('')
            lines.append('  DELCARE_UNMARSHALL(%s) {' % self.name)
            # TODO: implement unmarshall auto gen
            lines.append('  }')
            lines.append('')
        # for members
        lines.append(' private:')
        for f in self.fields:
            lines.append('  %s %s;' % (f.type.name, f.name))
        
        lines.append('};')
        lines.append('')
        return lines
    
    def mapToLines(self):
        classBody = self.prepareClassBody()
        
        # for license comments
        self.lines.append(LICENSE)

        product = self.package.product.lower().replace('-', '_')
        version = 'v' + self.package.version.replace('-', '')
        self.path = '%s/%s/%s/%s.h' % (self.root, product, version, self.name)

        # start header guard
        guard = '%s_%s_%s_' % (product.upper(), version.upper(), self.name.upper())
        self.lines.append('#ifndef ' + guard)
        self.lines.append('#define ' + guard)
        self.lines.append('')

        # for include lines
        includes = self.sortIncludes()
        for i in includes:
            self.lines.append('#include %s' % i)
        self.lines.append('')

        # start namespaces
        self.lines.append('namespace aliyun { ')
        self.lines.append('namespace %s {' % product)
        self.lines.append('namespace %s {' % version)
        self.lines.append('')

        # place class body
        self.lines.extend(classBody)

        # end namespaces
        self.lines.append('}  // namespace ' + version)
        self.lines.append('}  // namespace ' + product)
        self.lines.append('}  // namespace aliyun')
        self.lines.append('')

        # end header guard
        self.lines.append('#endif  // ' + guard)

    def dump(self):
        print '>>>', self.path
        self.mapToLines()
        for f in self.fields:
            print f.name, f.type
        print ''
        for line in self.lines:
            print line

    def write(self):
        self.mapToLines()
        dname = os.path.dirname(self.path)
        if not os.path.exists(dname):
            os.makedirs(dname)
        fd = open(self.path, 'w')
        for line in self.lines:
            fd.write(line + '\n')
        fd.close()

class EnumType:
    def __init__(self, name, values):
        self.name = name
        self.values = values

    # checking meta is validate enum 
    @staticmethod
    def validate(meta):
        if meta.get('valueSwitch') == None:
            return False
        if meta.get('valueSwitch').get('cases') == None:
            return False
        return True

    @staticmethod
    def create(meta):
        name = meta['tagName']
        if meta['type'] == 'Boolean':
            print 'WARNING: > boolean enum:', meta
            assert len(meta['valueSwitch']['cases']) == 2
            return lookupType('Boolean')
        if meta['type'] != 'String':
            print meta
        values = []
        for v in meta['valueSwitch']['cases']:
            values.append(v['tagValue'])
        if len(values) == 2:
            values.sort()
            if values == ['false', 'true']:
                print 'WARNING: > boolean enum:', meta
                return lookupType('Boolean')
        print 'ENUM >', name, values
        return EnumType(name, values) 
    
    @staticmethod
    def extractEnumsHelper(enums, obj):
        name = obj.get('tagName')
        valueMap = obj.get('valueSwitch')
        if name != None and valueMap != None and valueMap.get('cases') != None:
            oldEnum = enums.get(name)
            newEnum = EnumType.create(obj)
            if oldEnum != None:
                assert oldEnum.name == newEnum.name
                print '> duplicate enum descriptions: ', name
#                 raise Exception('duplicate enum')
            enums[name] = newEnum
        for k in obj:
            if type(obj[k]) == dict:
                EnumType.extractEnumsHelper(enums, obj[k])
            elif type(obj[k]) == list:
                for i in obj[k]:
                    EnumType.extractEnumsHelper(enums, i)
    
    # extract enums recusivly
    @staticmethod
    def extract(obj):
        enums = dict()
        EnumType.extractEnumsHelper(enums, obj)
        return enums
    
    def __repr__(self):
        return self.__str__()

    def __str__(self):
        return '{name: %s, values: %s}' % (self.name, self.values)
    
    # depends <string>, <string.h>
    # @return list contain mapped code lines
    def mapToCode(self, feed):
        lines = []
        lines.append('class %s {' % self.name)
        lines.append(' public:')
        # enum constants
        lines.append('  enum {')
        for v in self.values:
            lines.append('    %s,' % v.upper())
        lines.append('  };')
                
        # default ctor for default value
        lines.append('  explicit %s(int v) : value_(v) {}' % self.name)
        
        # ctor for construct form string
        lines.append('  %s(std::string str) {')
        lines.append('    for (value_ = 0; value_ < %s; value_++) {' % len(self.values))
        lines.append('      if (::strncasecmp(str.c_str(), this->toString(), str.length()) == 0) {')
        lines.append('        break;')
        lines.append('      }')
        lines.append('    }')
        lines.append('  }')
        
        # toString()
        lines.append('  const char* toString() {')
        lines.append('    const char* names[] = {')
        for v in self.values:
            lines.append('    "%s",' % v.upper())
        lines.append('    };')
        lines.append('    return value_ < sizeof(names)/sizeof(names[0]) ? names[value_] : NULL;' )
        lines.append('  }')
        
        lines.append(' private:')
        lines.append('  int value_;')
        lines.append('};')
        for i in range(len(lines)):
            lines[i] = feed * ' ' + lines[i]
        return lines
    
    # get include depends
    @staticmethod
    def getDepends():
        return ['<string.h>', '<string>']
    

# for array<string>
class ListType:
    def __init__(self, name, item):
        self.name = name

    @staticmethod
    def validate(meta):
        keys = meta.keys()
        keys.sort()
        return keys == ['itemName', 'tagName']

    @staticmethod
    def create(meta):
        name = meta['tagName']
    
    def mapToCode(self, feed):
        return [feed * ' ' + ('std::vector<std::string> %s;' % self.name)]
    
    @staticmethod
    def getDepends():
        return ['<string>', '<vector>']

# for array<T>
class ArrayType:
    def __init__(self):
        pass

    @staticmethod
    def validate(meta):
        keys = meta.keys()
        keys.sort()
        return keys == ['itemName', 'members', 'tagName'] and meta['members'] != None

    @staticmethod
    def create(meta):
        pass
    
    def mapToCode(self, feed):
        

    @staticmethod
    def getDepends():
        return ['<vector>']

class StructType:
    def __init__(self):
        pass

    @staticmethod
    def create(meta):
        pass
        
    def mapToCode(self, feed):
        pass
    
    @staticmethod
    def getDepends():
        return []


class CodeGenerator(object):

    def __init__(self, codedir, metadir):
        self.codedir = codedir
        self.metadir = metadir

        # list of PackageInfo
        self.packages = []
        self.apikeys = set()
        self.pkgkeys = set()
        self.apis = dict()
        self.pkgs = dict()
        self.reqkeys = set()
        self.respkeys = set()
        self.apicomkeys = set()
        self.reqs = dict()
        self.resps = dict()
        self.apicoms = dict()

        # parser context
        self.file = None

    # find package by product version
    def findPackage(self, product, version=None):
        for p in self.packages:
            if (p.product == product) and (version == None or p.version == version):
                return p
        return None

    def foreach(self, obj, prefix, path):
        for k in obj:
            p = prefix + '.' + k
            if type(obj[k]) == dict:
                self.foreach(obj[k], p, path)
            elif type(obj[k]) == list:
                for i in obj[k]:
                    self.foreach(i, p, path)
            else:
                if self.apicoms.get(p) == None:
                    self.apicoms[p] = dict()
                self.apicoms[p][path] = obj[k]

    def parseJson(self, path, basename):
        # TODO: parsing and generate code file

        prefix = path.find(API_PATH_PREFIX)
        if prefix == -1:
            print 'no', API_PATH_PREFIX, 'in', path
            sys.exit(-1)

        # took product and version
        pvstr = path[prefix + len(API_PATH_PREFIX) : ]

        # read file content
        fobj = open(path)
        fobj.seek(0, 2)
        flen = fobj.tell()
        fobj.seek(0, 0)
        fctx = fobj.read(flen)

        # replace special chars
        fctx = fctx.replace('\n', '').replace('\t', '')

        # load to json object
        jobj = json.loads(fctx)

        if path.endswith(VERSION_INFO):
            # for pacakge
            apiList = []
            for a in jobj.get('apis').get('apis'):
                apiList.append(a['name'])
            apiList.sort()
            product = jobj['product']
            version = jobj['name']
            assert len(product) > 0 and len(version) > 0
            pkg = PackageInfo(path, product, version, jobj['apiStyle'])
            pkg.addApiList(apiList)
            self.packages.append(pkg)

            # trace info
            for k in jobj:
                self.pkgkeys.add(k)
                if self.pkgs.get(k) == None:
                    self.pkgs[k] = {pvstr : str(jobj[k])}
                else:
                    self.pkgs[k][pvstr] = str(jobj[k])
        else:
            # api
            product = jobj['product']
            version = jobj['version']
            vproto = jobj.get('isvProtocol')
            methods = vproto['method'].split('|')
            protocols = vproto['protocol'].split('|')
            api = API(basename, product, version, methods, protocols, vproto.get('pattern'))

            # for request class
            pkg = self.findPackage(product, version)
            request = ApiClass(self.codedir, api, basename, True, pkg)
            for param in jobj.get('parameters').get('parameters'):
                if param['type'] == 'List':
                    # workaroud a meta error
                    print '>>> replace: List => Inetger', param, path
                    param['type'] = 'Integer'
                request.addField(param['type'], param['tagName'])
            request.write()
            
            # for reponse class
            response = ApiClass(self.codedir, api, basename, False, pkg)

            # for result mapping
            resultMapping = jobj.get('resultMapping')
            if resultMapping.get('members') != None:
                for m in resultMapping['members']:
                    if m.get('valueSwitch') == None:
                        response.addField(m['type'], m['tagName'])
            
            try:
                enums = EnumType.extract(jobj)
            except Exception as e:
                print e.message, 'raised when parsing', path, traceback.format_exc()
#             response.dump()

            # trace info
            self.foreach(jobj, 'API', path)

            for k in jobj:
                self.apikeys.add(k)
                if self.apis.get(k) == None:
                    self.apis[k] = {pvstr : str(jobj[k])}
                else:
                    self.apis[k][pvstr] = str(jobj[k])

    def traversal(self, root):
        for child in os.listdir(root):
            # skip some file/directory
            if child.startswith('.'):
                continue
            
            path = root + '/' + child
            if os.path.isdir(path):
                self.traversal(path)
            else:
                if child.endswith(JSON_SUFFIX):
                    basename = child[ : -len(JSON_SUFFIX)]
                    self.parseJson(path, basename)
                else:
                    print "WARNING: ignore", path

    def generate(self):
        if not os.path.isdir(self.metadir):
            print "bad meta-dir"
            sys.exit(-1)
        self.traversal(self.metadir)

        print 'api compose keys:'
        apicomkeys = list(self.apicoms.keys())
        apicomkeys.sort()
        for i in apicomkeys:
            print i

        print 'api compose key details:'
        types = set()
        for k in self.apicoms:
            if k.find('type') != -1:
                types = types.union(self.apicoms[k].values())
                print k, '=>', set(self.apicoms[k].values()), '\n'
#            for i in apicoms[k]:
            if k.find('lists') != -1:
                print k, '=>', self.apicoms[k].values()
        print 'types: ', types
        
        longKey = 'API.resultMapping.structs.arrays.arrays.structs.members.tagName'
        print longKey, '\n', self.apicoms[longKey]


# print usage and exit program
def usage():
    print sys.argv[0], ''' meta-dir code-dir
    meta-dir -- root directory of meta data
    code-dir -- output directory to store generated code
    '''
    sys.exit(-1)


# program entry
if __name__ == '__main__':
    if len(sys.argv) < 3:
        usage()
    metadir = sys.argv[1]
    codedir = sys.argv[2]
    
    generator = CodeGenerator(codedir, metadir)
    generator.generate()

